﻿
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;

using Microsoft.Extensions.Options;

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

using Utils.WeChat.Helper;
using Utils.WeChat.Options;
using Utils.WeChat.PayResult;

namespace Utils.WeChat
{
    public class WeChatPay
    {
        private readonly IOptionsMonitor<WeChatOptions> options;

        private WeChatOptions Options => options.CurrentValue;

        private readonly HttpClient httpClient;

        private const string BASE_URL = "https://api.mch.weixin.qq.com";

        public WeChatPay(IOptionsMonitor<WeChatOptions> options)
        {
            this.options = options;
            httpClient = new HttpClient(new WxPayHttpHandler(Options.MerchantID, Options.SerialNo, Options.PrivateKey));
        }

        /// <summary>
        /// 下载证书
        /// </summary>
        /// <returns></returns>
        public async Task<List<CertificateResult>> DownloadCertificates()
        {
            var list = await httpClient.GetAsync(BASE_URL + "/v3/certificates").JsonResultAsync(ChangeResult<DateListResult<CertificateResult>>);
            return list.data;
        }

        #region 提现

        /// <summary>
        /// 到零钱
        /// </summary>
        /// <param name="appName"></param>
        /// <param name="partnerTradeNo"></param>
        /// <param name="amount"></param>
        /// <param name="openid"></param>
        /// <param name="realName"></param>
        /// <param name="desc"></param>
        /// <returns></returns>
        public async Task<string> PayBag(string appName, string partnerTradeNo, decimal amount, string openid, string realName, string desc)
        {
            if (!Options.Apps.ContainsKey(appName))
            {
                throw Error.SystemError("未知app节点");
            }
            var data = new
            {
                mchid = Options.MerchantID,
                mch_appid = Options.Apps[appName].AppID,
                partner_trade_no = partnerTradeNo,
                check_name = string.IsNullOrEmpty(realName) ? "NO_CHECK" : "FORCE_CHECK",
                re_user_name = realName,
                amount = Convert.ToInt32(amount * 100),
                openid,
                desc,
                nonce_str = Guid.NewGuid().ToString("N"),
                Options = Options.RefundNotifyURL
            };
            var result = await PostV2($"{BASE_URL}/mmpaymkttransfers/promotion/transfers", data, certificate: Options.GetCertificate());

            return result["payment_no"];
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="partner_trade_no">商户企业付款单号</param>
        /// <param name="amount">金额</param>
        /// <param name="bank_code">收款方开户行</param>
        /// <param name="enc_bank_no">银行卡号</param>
        /// <param name="enc_true_name">收款方用户名</param>
        /// <param name="desc">备注</param>
        public async Task<string> PayCard(string partnerTradeNo, decimal amount, string bankCode, string encBankNo, string encTrueName, string desc)
        {
            var pk = @"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA79nPbGlzD6xlVNSopt4l
pVCJouwYQhbw2THwpNx6pZbiZj4Ai/gSKAnN5bmZihHgkN3iUceVWyev28sYwz8m
Qt6qKE2oYvl98JKu+s9ihrUBsmjmIPVlKURAIjHyJomIWhX+3kcoyf5zYMt3PDcE
ShUL1AaN8Hf8aIedPRDSEIe/EOu51mtJXS/EQuVc9Iukt+hsVAnpBHWjEVxqao8r
sGa6qtlh+giiOsjAurESkknC5YHqtB5U3L3Q6T20IyK84f9A/iBSX8E8CpbPwByd
bkDC3vjgz7DUhSCIKiVOYJIeKkicCosdLLyyf1WtVD3znA9bN+GihN2n1sF0uLYc
4wIDAQAB";
            var data = new
            {
                mchid = Options.MerchantID,
                partner_trade_no = partnerTradeNo,
                bank_code = bankCode,
                enc_bank_no = RSAFromPkcs8.RSAEncrypt(encBankNo, pk, null, false),
                enc_true_name = RSAFromPkcs8.RSAEncrypt(encTrueName, pk, null, false),
                amount = Convert.ToInt32(amount * 100),
                desc,
                nonce_str = Guid.NewGuid().ToString("N")
            };
            var result = await PostV2($"{BASE_URL}/mmpaysptrans/pay_bank", data, certificate: Options.GetCertificate());
            return result["payment_no"];
        }
        #endregion


        #region 下单支付

        /// <summary>
        /// 下单 Jsapi
        /// </summary>
        /// <param name="appid"></param>
        /// <param name="attach"></param>
        /// <param name="out_trade_no"></param>
        /// <param name="description"></param>
        /// <param name="amount_total"></param>
        /// <param name="openid"></param>
        /// <param name="notify_url"></param>
        /// <returns></returns>
        public Task<PrepayResult> OrderJsapi(string appName, string out_trade_no, string description, decimal amount_total,
            string openid, string attach = "", string notify_url = null)
        {
            if (string.IsNullOrWhiteSpace(description))
            {
                throw new ArgumentException($"“{nameof(description)}”不能为 null 或空白。", nameof(description));
            }

            if (!Options.Apps.ContainsKey(appName))
            {
                throw Error.SystemError("未知app节点");
            }

            return httpClient.PostAsync(BASE_URL + "/v3/pay/transactions/jsapi", new JsonContent(new
            {
                mchid = Options.MerchantID,
                out_trade_no,
                appid = Options.Apps[appName].AppID,
                attach,
                description = description.SubstringByte(0, 120),
                amount = new
                {
                    total = (int)(amount_total * 100),
                    currency = "CNY"
                },
                payer = new
                {
                    openid
                },
                //time_expire = DateTime.Now.AddMinutes(30).ToString("yyyy-MM-ddTHH:mm:sszzz"),
                notify_url = notify_url ?? Options.NotifyURL,
            }))
                .JsonResultAsync(ChangeResult<PrepayResult>);
        }

        /// <summary>
        /// 合单支付
        /// </summary>
        /// <param name="appid"></param>
        /// <param name="combine_out_trade_no"></param>
        /// <param name="combineSubs"></param>
        /// <param name="openid"></param>
        /// <param name="notify_url"></param>
        /// <returns></returns>
        public Task<PrepayResult> CombineOrderJsapi(string appName, string combine_out_trade_no, CombineSubOrder[] combineSubs, string openid, string notify_url = null)
        {
            if (!Options.Apps.ContainsKey(appName))
            {
                throw Error.SystemError("未知app节点");
            }
            return httpClient.PostAsync(BASE_URL + "/v3/combine-transactions/jsapi", new JsonContent(new
            {
                combine_out_trade_no,
                combine_mchid = Options.MerchantID,
                combine_appid = Options.Apps[appName].AppID,
                sub_orders = combineSubs.Select(a => new
                {
                    mchid = Options.MerchantID,
                    a.out_trade_no,
                    a.attach,
                    description = a.description.SubstringByte(0, 126),
                    amount = new
                    {
                        total_amount = (int)(a.total_amount * 100),
                        currency = "CNY"
                    }
                }).ToArray(),
                combine_payer_info = new
                {
                    openid
                },
                //time_expire = DateTime.Now.AddMinutes(30).ToString("yyyy-MM-ddTHH:mm:sszzz"),
                notify_url = notify_url ?? Options.NotifyURL,
            }))
                .JsonResultAsync(ChangeResult<PrepayResult>);
        }

        /// <summary>
        /// 创建预支付包
        /// </summary>
        /// <param name="prepay_id"></param>
        /// <returns></returns>
        public Dictionary<string, string> CreatePayObj(string appName, string prepay_id)
        {
            if (!Options.Apps.ContainsKey(appName))
            {
                throw Error.SystemError("未知app节点");
            }
            var data = new Dictionary<string, string>
            {
                { "appId", Options.Apps[appName].AppID },
                { "timeStamp", DateTime.Now.ToUnixTimestamp().ToString() },
                { "nonceStr", Guid.NewGuid().ToString("N") },
                { "package", "prepay_id=" + prepay_id }
            };

            string paySign = Cryptography.RSASign(Options.PrivateKey, string.Join('\n', data.Values) + '\n');
            data.Add("signType", "RSA");
            data.Add("paySign", paySign);
            return data;
        }


        /// <summary>
        /// 查询交易
        /// </summary>
        /// <param name="transaction_id"></param>
        public Task<JObject> QueryTransactions(string out_trade_no)
        {
            return httpClient.GetAsync($"{BASE_URL}/v3/pay/transactions/out-trade-no/{out_trade_no}?mchid={Options.MerchantID}")
                    .JsonResultAsync(ChangeResult<JObject>);
        }


        /// <summary>
        /// 
        /// </summary>
        /// <param name="out_refund_no"></param>
        /// <returns></returns>
        public Task<JObject> QueryRefund(string out_refund_no)
        {
            return httpClient.GetAsync($"{BASE_URL}/v3/refund/domestic/refunds/{out_refund_no}?mchid={Options.MerchantID}")
                    .JsonResultAsync(ChangeResult<JObject>);
        }


        #endregion

        /// <summary>
        ///  订单退款
        /// </summary>
        /// <param name="appEnum"></param>
        /// <param name="out_trade_no"></param>
        /// <param name="out_refund_no"></param>
        /// <param name="reason"></param>
        /// <param name="refund"></param>
        /// <param name="total"></param>
        /// <returns></returns>
        public Task<RefundResult> Refund(string appName, string out_trade_no, string out_refund_no, string reason, decimal refund, decimal total)
        {
            if (!Options.Apps.ContainsKey(appName))
            {
                throw Error.SystemError("未知app节点");
            }
            return httpClient.PostAsync(BASE_URL + "/v3/refund/domestic/refunds", new JsonContent(new
            {
                out_trade_no,
                out_refund_no,
                reason,
                notify_url = Options.RefundNotifyURL,
                amount = new
                {
                    refund = (int)(refund * 100),
                    total = (int)(total * 100),
                    currency = "CNY"
                }
            }))
                .JsonResultAsync(ChangeResult<RefundResult>);
        }
        public Task<RefundResult> RefundByTransaction(string appName, string transaction_id, string out_refund_no, string reason, decimal refund, decimal total)
        {
            if (!Options.Apps.ContainsKey(appName))
            {
                throw Error.SystemError("未知app节点");
            }
            return httpClient.PostAsync(BASE_URL + "/v3/refund/domestic/refunds", new JsonContent(new
            {
                transaction_id,
                out_refund_no,
                reason,
                notify_url = Options.RefundNotifyURL,
                amount = new
                {
                    refund = (int)(refund * 100),
                    total = (int)(total * 100),
                    currency = "CNY"
                }
            }))
                .JsonResultAsync(ChangeResult<RefundResult>);
        }
        #region 回调

        public async Task<(string eventType, JObject result)> RefundNotify(Stream stream)
        {
            using var reader = new StreamReader(stream, Encoding.UTF8);
            string str = await reader.ReadToEndAsync();
            // Console.WriteLine("支付结果通知：" + str);
            var result = JsonConvert.DeserializeObject<NotifyResult>(str);
            var dataStr = result.Decrypt(Options.Key);
            //Console.WriteLine("解析结果：" + dataStr);
            return (result.event_type, JObject.Parse(dataStr));
        }

        public async Task<JObject> Notify(Stream stream)
        {
            using var reader = new StreamReader(stream, Encoding.UTF8);
            string str = await reader.ReadToEndAsync();
            Console.WriteLine("支付结果通知：" + str);
            var result = JsonConvert.DeserializeObject<NotifyResult>(str);
            if (!result.TransactionSuccess())
            {
                throw Error.SystemError(result.summary);
            }
            var dataStr = result.Decrypt(Options.Key);
            Console.WriteLine("解析结果：" + dataStr);
            return JObject.Parse(dataStr);
        }

        #endregion



        #region 私有

        private Task<IDictionary<string, string>> PostV2(string uri, object body, object queryParams = null, X509Certificate certificate = null)
        {
            return PostV2<IDictionary<string, string>>(uri, body, queryParams, certificate);
        }


        private async Task<T> PostV2<T>(string uri, object body, object queryParams = null, X509Certificate certificate = null)
        {
            using var httpClient2 = new HttpClient(new WxPayHttpHandler2(certificate));
            string xml = BuilderXmlAndSign(body, Options.Key);
            var responseMessage = await httpClient2.PostAsync(uri.AppendQuery(queryParams), new StringContent(xml, Encoding.UTF8, "text/xml"));
            return await responseMessage.JsonResultAsync(ChangeResult2<T>);
        }


        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="content"></param>
        /// <returns></returns>
        private async Task<T> ChangeResult<T>(HttpResponseMessage response)
        {
            if (!response.IsSuccessStatusCode)
            {
                var respStr = await response.Content.ReadAsStringAsync();
                var error = JsonConvert.DeserializeObject<WeChatResult>(respStr);
                if (error.code != "SUCCESS")
                {
                    throw Error.SystemError(error.message);
                }
            }
            string str = await response.Content.ReadAsStringAsync();
            return str.JSONDeserialize<T>();
        }

        private async Task<T> ChangeResult2<T>(HttpResponseMessage response)
        {
            if (!response.IsSuccessStatusCode)
            {

                throw Error.SystemError("微信支付响应失败，状态码：" + (int)response.StatusCode);
            }
            string xmlStr = await response.Content.ReadAsStringAsync();
            var result = xmlStr?.XmlToDictionary();
            if (result == null)
            {
                throw Error.SystemError("微信支付响应失败:无返回值");
            }
            if (!"SUCCESS".Equals(result.TryGetValue("return_code")))
            {
                throw Error.SystemError(result.TryGetValue("return_msg", "微信支付响应失败"));
            }
            if (!"SUCCESS".Equals(result.TryGetValue("result_code")))
            {
                throw Error.SystemError(result.TryGetValue("err_code_des", "微信支付响应失败"));
            }
            if (result is T r)
            {
                return r;
            }
            return result.JsonCopy<T>();
        }


        private static string BuilderXmlAndSign(object body, string key)
        {
            var dict = body.ToDictionary().Where(a => a.Value != null).OrderBy(a => a.Key).ToDictionary(a => a.Key, a => a.Value.ToString());

            //待签名字符
            string signStr = dict.Where(a => !a.Value.IsNullOrWhiteSpace() && a.Key != "sign")
                .Select(a => a.Key + "=" + a.Value)
                .Join("&");
            signStr += "&key=" + key;
            dict.Add("sign", signStr.ToMD5().ToUpper());

            return ToXml(dict);
        }

        /// <summary>
        /// 转xml
        /// </summary>
        /// <param name="dict"></param>
        /// <returns></returns>
        private static string ToXml(Dictionary<string, string> dict)
        {
            var builder = new StringBuilder("<xml>");
            foreach (var item in dict)
            {
                //字段值不能为null，会影响后续流程
                if (item.Value == null) continue;
                builder.AppendFormat(
                   decimal.TryParse(item.Value, out decimal _) ? "<{0}>{1}</{0}>" : "<{0}><![CDATA[{1}]]></{0}>", item.Key,
                    item.Value);
            }
            builder.Append("</xml>");
            return builder.ToString();
        }

        #endregion
    }

    public enum TradeType
    {
        JSAPI,
        Native,
        APP,
        MWEB
    }
}
